iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0

💡 今日學習目標:理解 OWASP A04 加密機制失效的常犯錯誤,學會先判斷資料該用哪種保護方向,再掌握密碼雜湊的正確參數與舊系統遷移做法。


📌 前言:資料洩漏的最後一道防線

當資料庫不幸被拖庫(Dump)或被非法存取時,決定企業是「有驚無險」還是「賠償下架」的關鍵,就在於資料是否經過正確的加密與保護

這個類別在 2021 年叫 A02(由更早的「敏感資料外洩」改名而來),2025 年下降到 A04,但別誤會,下降不代表它變得不重要,只是因為 Security Misconfiguration 與 Software Supply Chain 這兩項在資料上衝得更前面。

敏感資料之所以外洩,根源往往在於:密碼學演算法用錯、用了太快的雜湊,或者根本忘了保護


🔍 核心原理:兩個維度,兩種保護

密碼學防禦要分成兩個場景來看:

傳輸中(Data in Transit) 靜止中(Data at Rest)
威脅 中間人竊聽、封包側錄 資料庫被拖走、備份檔外流
做法 全站強制 HTTPS,最低 TLS 1.2、建議 1.3 依資料性質選擇雜湊或加密
禁止 明文 HTTP、FTP、Telnet 明文儲存任何敏感欄位

傳輸中的部分相對單純:把 HTTPS 開好、把明文協定關掉,大部分工作就完成了(Day 19 談 HSTS 時會再補強)。

真正容易出事、而且出事就無法挽回的,是靜止中的保護


🧭 動手之前先做一個判斷:這筆資料該往哪個方向保護?

很多人一想到「保護資料」就直接開始查「哪個加密演算法比較強」。但那是第二步。

第一步應該是問一個更基本的問題:

敏感資料保護決策圖:先判斷需不需要還原原文,再選演算法

為什麼方向比演算法重要?因為:

  • 演算法選錯還能換:bcrypt 覺得不夠好,之後可以逐步遷移到 Argon2id。
  • 方向選錯就完了:如果你把使用者密碼做成可逆加密,那麼金鑰一旦外洩,攻擊者拿到的是一整份可以還原的明文密碼表。而使用者在別的網站往往用同一組密碼。

那個第三欄「那就不要存」特別值得停下來想一想。它常常被跳過,但它其實是三個選項裡最安全的:你手上根本沒有的資料,永遠不會從你這裡外洩


🛠️ 密碼儲存的黃金法則 (Password Hashing Practice)

新手開發者在儲存使用者密碼時,最容易踩到以下四個陷阱:

  1. 陷阱 1:儲存明文密碼(Plaintext)➔ 被拖庫就直接結束,沒有任何緩衝。
  2. 陷阱 2:用 MD5 / SHA-1 / SHA-256 直接雜湊,而且沒加鹽 ➔ 攻擊者拿現成的彩虹表一查就出來。
  3. 陷阱 3:加了鹽,但還是用 SHA-256 這種快速雜湊 ➔ 彩虹表確實失效了,但擋不住GPU 暴力破解,現代顯卡每秒可以算出數十億次 SHA-256。
  4. 陷阱 4:所有使用者共用同一個固定 Salt ➔ 等於沒加鹽。攻擊者只要針對這一個鹽值重新算一次表,就能同時破解所有帳號。

💡 陷阱 2 跟 3 的差別很重要,很多文章會混為一談
加鹽解決的是「同一個密碼產生同樣的雜湊值,可以被預先算表查出來」;
慢速演算法解決的是「就算要一個一個猜,也猜得夠慢」。

這是兩個獨立的問題,必須兩個都解決。而 Argon2id 與 bcrypt 的價值就在於:它們把這兩件事一次做完了。


❌ 不安全的密碼處理邏輯

// ❌ 錯誤範例:使用沒有 Salt 的 SHA256,且演算法速度過快(易被 GPU 暴破)
Function SaveUserPassword(username, rawPassword):
    HashedPassword = SHA256(rawPassword)
    DB.Save(username, HashedPassword)

✅ 安全的密碼處理邏輯

安全密碼儲存的通則:使用專用的慢速單向密碼雜湊演算法 (Password Hashing Functions),並搭配每個使用者隨機產生的 Salt

這些演算法具備「工作因子(Work Factor)」與「記憶體硬度(Memory Hardness)」,可以刻意把單次計算拉長到數十毫秒:對正常登入完全無感,但讓大規模暴力破解的成本飆升好幾個數量級

建議參數(依 OWASP 密碼儲存指引)

演算法 建議參數 使用時機
Argon2id m=19456(19 MiB)、t=2p=1 首選,新專案直接用這個
bcrypt work factor ≧ 10 既有系統、或函式庫只支援它時
PBKDF2-HMAC-SHA256 600,000 次迭代 需要符合 FIPS 合規要求時
// ✅ 正確範例:使用 Argon2id 或 bcrypt
Function SaveUserPassword(username, rawPassword):
    // 演算法會自己產生密碼學安全的隨機 Salt,
    // 並把 Salt 與參數一起封裝進輸出字串裡
    SecureHash = Argon2id.Hash(rawPassword, Memory=19456, Iterations=2, Parallelism=1)
    
    DB.Save(username, SecureHash)   // 不需要另開 Salt 欄位

Function VerifyUserPassword(rawPassword, storedHash):
    // 函式庫會自動解析出 Salt 與參數,並使用「時間常數比對」避免時序側錄
    Return Argon2id.Verify(rawPassword, storedHash)

⚠️ 如果你用 bcrypt,有一個很多人不知道的坑:它只吃前 72 個位元組。

超過的部分會被直接截斷。也就是說,一個 80 字元的強密碼,跟它的前 72 字元,在 bcrypt 眼中是同一組密碼。
如果你的系統允許長密碼(或使用者可能貼上密碼管理器產生的長字串),這點務必注意,這也是新專案建議直接選 Argon2id 的原因之一。


🔧 那我現有的系統已經存了一堆 MD5,怎麼辦?

這是最實際的問題,而且答案不是「叫所有使用者改密碼」,因為你手上沒有原始密碼,無法直接重算

業界有兩種標準做法:

做法一:包一層新的雜湊上去(推薦)

既然無法還原原始密碼,那就把舊的雜湊值當成輸入,再用新演算法包一層

// 遷移期:把既有的 md5 值,用 bcrypt 再包一次
newHash = bcrypt.Hash( existingMd5Hash, cost=12 )
DB.UpdateUserHash(userId, newHash, algorithm="bcrypt(md5)")

驗證時就照著同樣的順序做:先算 md5(輸入的密碼),再丟給 bcrypt 驗證。

這個做法的好處是:你可以在一個晚上就把整張使用者表升級完,不需要任何使用者配合。

然後,等使用者下次登入時,你手上就有原始密碼了。這時直接用純 Argon2id 重算一次、換掉那筆複合雜湊,並在資料庫記下他用的是哪一種演算法。

做法二:讓長期未登入的帳號重設密碼

對於一兩年沒登入的殭屍帳號,最乾脆的做法就是直接刪掉舊雜湊、強制走忘記密碼流程

🛡️ 關鍵是過渡期要能同時支援新舊兩種演算法。在使用者資料表加一個 hash_algorithm 欄位,登入時依照它決定用哪套邏輯驗證,這樣你就能平滑地一個一個換過去,而不是某天半夜全站切換、然後祈禱不要出事。


🎯 今日重點小結與防守心法

  • 🔹 心法 1:先選方向,再選演算法。問自己「以後需不需要還原原文」。答錯這一題,後面選什麼演算法都救不回來。
  • 🔹 心法 2:密碼永遠不加密,只做慢速加鹽雜湊。加鹽擋彩虹表、慢速擋 GPU 暴破,兩件事都要做,而 Argon2id 一次幫你解決。
  • 🔹 心法 3:最安全的資料,是你根本沒存的那一筆。信用卡交給金流商、身分驗證交給 OAuth,能不碰的敏感資料就別碰。

💬 明日預告:【Day 18】【動手做】OWASP A05 注入攻擊:SQLi 防衛與參數化查詢
明天我們要拆解這個最長壽的資安殺手,看看參數化查詢究竟是怎麼從底層讓它徹底失效的!


上一篇
【Day 16】【動手做】OWASP A01 權限控制失效:IDOR 越權存取攻防實戰
下一篇
【Day 18】【動手做】OWASP A05 注入攻擊:SQLi 防衛與參數化查詢
系列文
槍林彈雨下的資安防守:從品質觀念切入,帶開發者從零動手作資安 30 天22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
AndyAWD
iT邦新手 1 級 ‧ 2026-09-05 23:30:25

終於知道為什麼我太久沒登入就要我設定密碼了

我要留言

立即登入留言